 #-*- coding: utf-8 -*-

 ###############################################
 ##                                             ##
 ##  cfdi                                        ##
 ##                                              ##
 ##                                              ##
 ##                                              ##
 ##  by Críptidos Digitales                      ##
 ##  GPL (c)2010                                 ##
  ##                                             ##
    ###############################################

"""
This module contents the clases and methods for creation and management of
Comprobante Fiscal Digital por Internet, required by SAT México

Asumes existence of cfdv2.xsd or cfvd3.xsd in the same path
Asumes existence of the files passed as arguments
"""

import sys
from StringIO import StringIO
import base64
import chilkat
import M2Crypto
import hashlib
from lxml import etree

from ordereddict import OrderedDict


def createFactura(data, **kwds):
    msg = ''
    factura = None
    
    if not data:
        msg += 'No data - '
    
    if 'version' in kwds:
        version = kwds['version']
    else:
        msg += 'Missing cfd version - '
    
    if not 'archivoDeCertificado' in kwds:
        msg += 'Missing certificate path - '
    
    if not 'archivoDeLlave' in kwds:
        msg += 'Missing seal path - '
        
    if not 'contrasena' in kwds:
        msg += 'Missing private key password - '
        
    if not msg:
        xml, msg = parse(version, data)
        
        if not msg:
            msg = applyCertificate(version, xml, certificatePath=kwds['archivoDeCertificado'])
        
            if not msg:
                msg = applySeal(version, xml, keyPath=kwds['archivoDeLlave'], passwordPath=kwds['contrasena'])
        
        if msg:
            return None, msg
        
        factura = etree.tostring(xml, xml_declaration=True, encoding="UTF-8", pretty_print=True)

    return factura, msg



def applySeal(version, xml, seal=None, keyPath=None, passwordPath=None):
    """ Applies seal to the corresponding element in the etree.
    If not given, calculates one if posible.
    """
    if not seal:
        cadenaOriginal = getCadenaOriginal(version, xml)
        seal = getSeal(cadenaOriginal, keyPath, passwordPath)
    
    xml.set('sello', seal)



def applyCertificate(version, xml, certificate=None, certificatePath=None):
    """ Applies certificate to the corresponding element in the etree.
        If not given, calculates one if possible.
    """
    if not certificate:
        if certificatePath:
            certificate = base64.b64encode(open(certificatePath).read().replace('/n', ''))
        else:
            return "Can't find certificate"
    
    xml.set('certificado', certificate)



def getCadenaOriginal(version, xml):
    """ Calculates cadenaOriginal
        xml is etree.element
    """
    cadenaOriginal = str(etree.XSLT(etree.parse('cadenaoriginal_%s_0.xslt' % version))(xml))
    
    return cadenaOriginal



def getSeal(cadenaOriginal, keyPath, passwordPath):
    """ Calculates seal
    """
    
    cadenaOriginalMd5 = hashlib.md5(cadenaOriginal).digest()
    
    passwordLine = open(passwordPath).read()
    password = passwordLine[passwordLine.rfind(' ')+1:]
    
    der_key = chilkat.CkPrivateKey()
    der_key.LoadPkcs8EncryptedFile(keyPath, password)
    
    pem_key = der_key.getRsaPem()
    
    key = M2Crypto.RSA.load_key_string(pem_key)
    
    signed = key.sign(cadenaOriginalMd5, 'md5')

    signed64 = base64.b64encode(signed)
    
    seal = signed64
    
    return seal



def parse(version, data):
    """
    Creates and returns a etree.Element that contains the atributes and 
    elements of a Comprobante
    """
    def cicle(data, parent):            
        for item in data:
            if type(data[item]) == OrderedDict:
                if item == "TimbreFiscalDigital":
                    X = etree.Element("{http://www.sat.gob.mx/TimbreFiscalDigital}TimbreFiscalDigital", nsmap={"tfd":"http://www.sat.gob.mx/TimbreFiscalDigital"})
                else:
                    X = etree.Element("{http://www.sat.gob.mx/cfd/%s}%s" % (version, item), nsmap={"cfdi":"http://www.sat.gob.mx/cfd/%s" % version})
                    
                parent.append(X)
                cicle(data[item], X)
            else:
                parent.set(item, data[item])

    def isValid():
        """
        Validate XML against XSD
        """
        schema = etree.XMLSchema(etree.parse(StringIO(open('cfdv%s.xsd' % version).read())))
        
        valid = True
        msg = None
        try:
            schema.assert_(xml)
        except:
            valid = False
            msg = sys.exc_info()[1]
            
        return valid, msg
        
    ## Documento
    xml = etree.Element("{http://www.sat.gob.mx/cfd/%s}Comprobante" % version, nsmap={"cfdi":"http://www.sat.gob.mx/cfd/%s" % version})
    xml.set("{http://www.w3.org/2001/XMLSchema-instance}schemaLocation", "http://www.sat.gob.mx/cfd/%s/cfdv%s.xsd" % (version, version))

    cicle(data, xml)
    
    valid = isValid()
    
    if valid[0]:
        return xml, None
    else:
        return None, valid[1]



def save(xmlString, filename):
    file = open(filename, 'w')
    file.write(xmlString)
    file.close()



